# -*- coding: utf-8 -*-
# @Time    : 2024/7/11 17:23
# @Author  : yujiahao
# @File    : 16_fastapi_response_model_2.py
# @description:更多模型

from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

# todo 多个模型

# 千万注意，什么情况下都不能有明文密码，要存储可以进行验证的安全哈希值 https://fastapi.tiangolo.com/zh/tutorial/security/simple-oauth2/#password-hashing
# 下面的代码展示了不同模型处理密码字段的方式，及使用位置的大致思路：


app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserInDB(BaseModel):
    username: str
    hashed_password: str
    email: EmailStr
    full_name: str | None = None


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved


# todo **user_in.dict() 简介
'''
本质就一个解包的操作，下面简单看一下：

1. Pydantic 的 .dict() 方法
   user_in 是类 UserIn 的 Pydantic 模型。
   Pydantic 模型支持 .dict() 方法，能返回包含模型数据的字典。
   
   示例：
   user_in = UserIn(username="john", password="secret", email="john.doe@example.com")
   可以调用：
   user_dict = user_in.dict()
   
   现在，变量 user_dict 中包含的是数据字典（变量 user_dict 是字典，不是 Pydantic 模型对象）。
   
   输出：
   print(user_dict)
   结果：
   {
       'username': 'john',
       'password': 'secret',
       'email': 'john.doe@example.com',
       'full_name': None,
   }

2. 解包 dict
   把字典 user_dict 以 **user_dict 形式传递给函数（或类），Python 会执行解包操作。
   它会把 user_dict 的键和值作为关键字参数直接传递。
   
   示例：
   UserInDB(**user_dict)
   
   结果：
   UserInDB(
       username="john",
       password="secret",
       email="john.doe@example.com",
       full_name=None,
   )
   
   或更精准地，直接把可能会用到的内容与 user_dict 一起使用：
   UserInDB(
       username = user_dict["username"],
       password = user_dict["password"],
       email = user_dict["email"],
       full_name = user_dict["full_name"],
   )

3. 用其它模型中的内容生成 Pydantic 模型
   从 user_in.dict() 中得到了 user_dict，下面的代码：
   user_dict = user_in.dict()
   UserInDB(**user_dict)
   
   等效于：
   UserInDB(**user_in.dict())
   
   因为 user_in.dict() 是字典，在传递给 UserInDB 时，把 ** 加在 user_in.dict() 前，可以让 Python 进行解包。
   这样，就可以用其它 Pydantic 模型中的数据生成 Pydantic 模型。

4. 解包 dict 和更多关键字
   接下来，继续添加关键字参数 hashed_password=hashed_password，例如：
   UserInDB(**user_in.dict(), hashed_password=hashed_password)
   
   输出结果：
   UserInDB(
       username = user_dict["username"],
       password = user_dict["password"],
       email = user_dict["email"],
       full_name = user_dict["full_name"],
       hashed_password = hashed_password,
   )

警告：
   辅助的附加函数只是为了演示可能的数据流，但它们显然不能提供任何真正的安全机制。

'''

# todo 减少重复

'''
    FastAPI 的核心思想就是减少代码重复。
    
    代码重复会导致 bug、安全问题、代码失步等问题（更新了某个位置的代码，但没有同步更新其它位置的代码）。
    
    上面的这些模型共享了大量数据，拥有重复的属性名和类型。
    
    FastAPI 可以做得更好。
    
    声明 UserBase 模型作为其它模型的基类。然后，用该类衍生出继承其属性（类型声明、验证等）的子类。
    
    所有数据转换、校验、文档等功能仍将正常运行。
    
    这样，就可以仅声明模型之间的差异部分（具有明文的 password、具有 hashed_password 以及不包括密码）。
    
    通过这种方式，可以只声明模型之间的区别（分别包含明文密码、哈希密码，以及无密码的模型）。
    
        class UserBase(BaseModel):
            username: str
            email: EmailStr
            full_name: str | None = None
        
        
        class UserIn(UserBase):
            password: str
        
        
        class UserOut(UserBase):
            pass
        
        
        class UserInDB(UserBase):
            hashed_password: str
'''

# todo Union 或者 anyOf
'''
响应可以声明为两种类型的 Union 类型，即该响应可以是两种类型中的任意类型。
在 OpenAPI 中可以使用 anyOf 定义。为此，请使用 Python 标准类型提示 typing.Union

在 FastAPI 中，确实可以通过使用 Python 的 typing.Union 来实现 OpenAPI 中 anyOf 的效果。
这样定义的响应模型可以是两种类型中的任意一种。FastAPI 会自动将其转换为 OpenAPI 规范中的 anyOf。

这个没啥滑头，新版本使用|也行，下面看例子，两个都一样。
笔记
    定义 Union 类型时，要把详细的类型写在前面，然后是不太详细的类型。
    下例中，更详细的 PlaneItem 位于 Union[PlaneItem，CarItem] 中的 CarItem 之前。
    
'''


class BaseItem(BaseModel):
    description: str
    type: str


class CarItem(BaseItem):
    type: str = "car"


class PlaneItem(BaseItem):
    type: str = "plane"
    size: int


items = {
    "item1": {"description": "All my friends drive a low rider", "type": "car"},
    "item2": {
        "description": "Music is my aeroplane, it's my aeroplane",
        "type": "plane",
        "size": 5,
    },
}


# 用Union和|效果一样
# @app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
@app.get("/items/{item_id}", response_model=PlaneItem | CarItem)
async def read_item_1(item_id: str):
    return items[item_id]


# todo 模型列表

# 使用同样的方式也可以声明由对象列表构成的响应。
# 为此，请使用标准的 Python typing.List：
class Item(BaseModel):
    name: str
    description: str


items = [
    {"name": "Foo", "description": "There comes my hero"},
    {"name": "Red", "description": "It's my aeroplane"},
]


@app.get("/items_2/", response_model=list[Item])
async def read_items_2():
    return items


# todo 任意 dict 构成的响应

'''
任意的 dict 都能用于声明响应，只要声明键和值的类型，无需使用 Pydantic 模型。

事先不知道可用的字段 / 属性名时（Pydantic 模型必须知道字段是什么），这种方式特别有用。

此时，可以使用 typing.Dict
'''


@app.get("/keyword-weights/", response_model=dict[str, float])
async def read_keyword_weights():
    return {"foo": 2.3, "bar": 3.4}
